﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Red.CommUtils;
using Red.EngineData.Helpers;
using Red.EngineData.View;

namespace Red.EngineData.Protocol.Interop
{
    /// <summary>
    /// Wrapper for storage information about interop expression, callbacks and debug info about interop execution process
    /// </summary>
    public class ActionWrapper
    {
        #region Properties

        /// <summary>
        /// Get interop expression
        /// </summary>
        public string Expression { get; private set; }

        /// <summarypublic
        /// On response callback
        /// </summary>
        public Action<InteropResult> OnResponse { get; private set; }

        /// <summary>
        /// On exception callback
        /// </summary>
        public Action<ProtocolException> OnException { get; private set; }

        /// <summary>
        /// Debug information about place where interop expression was called
        /// </summary>
        public InteropDebugInfo DebugInfo;

        /// <summary>
        /// If it is true, action is logged to interop debugger
        /// </summary>
        public bool IgnoreDebugger { get; private set; }

        #endregion

        #region Constructors

        /// <summary>
        /// Main constructor
        /// </summary>
        /// <param name="expression">Interop expression</param>
        /// <param name="onResponse">On response callback</param>
        /// <param name="onException">On exception callback</param>
        public ActionWrapper( string expression, Action<InteropResult> onResponse, Action<ProtocolException> onException )
        {
            Expression = expression;
            OnResponse = onResponse != null ? onResponse : result => { };
            OnException = onException != null ? onException : ex => { };
        }

        /// <summary>
        /// Additional constructor
        /// </summary>
        /// <param name="expression">Interop expression</param>
        /// <param name="onResponse">On response callback</param>
        /// <param name="onException">On exception callback</param>
        /// <param name="ignoreDebugger">If it is true, this action doesn't log anything to interop debugger</param>
        public ActionWrapper( string expression, Action<InteropResult> onResponse, Action<ProtocolException> onException, bool ignoreDebugger )
        {
            Expression = expression;
            OnResponse = onResponse != null ? onResponse : result => { };
            OnException = onException != null ? onException : ex => { };
            IgnoreDebugger = ignoreDebugger;
        }

        #endregion
    }

    public delegate void ViewUpdateEventHandler();

    /// <summary>
    /// Scheduler for data view update events.
    /// </summary>
    public interface IViewUpdateScheduler
    {
        void Schedule( ViewUpdateEventHandler handler );
    }

    /// <summary>
    /// Wrapper for notify event.
    /// </summary>
    internal class NotifierListeners
    {
        private event OnNotifyEventHandler _onNotifyEvent = null;

        public event OnNotifyEventHandler OnNotifyEvent
        {
            add
            {
                if ( _onNotifyEvent == null || !_onNotifyEvent.GetInvocationList().Contains( value ) )
                {
                    _onNotifyEvent += value;
                }
            }
            remove
            {
                _onNotifyEvent -= value;
            }
        }

        public void Notify( string evt )
        {
            _onNotifyEvent( evt );
        }
    }

    /// <summary>
    /// Allocates unique IDs for messages.
    /// </summary>
    public static class MessageIDAllocator
    {
        static long _nextFreeID = 1;

        public static ulong GetNewMessageID()
        {
            return (ulong)Interlocked.Increment( ref _nextFreeID );
        }
    }

    /// <summary>
    /// Interop type.
    /// </summary>
    public enum EInteropType
    {
        Exception = 0,
        Value = 1,
        Array = 2,
        Compound = 3,
        Functor = 4,
        Notifier = 5,
    }

    /// <summary>
    /// Event handlers for interop calls.
    /// </summary>
    public delegate void OnNotifyEventHandler( string evt );

    /// <summary>
    /// Protocol class for interop messages used for EngineData.
    /// </summary>
    public class InteropProtocol
    {
        /// <summary>
        /// Internal structure for scheduled actions.
        /// </summary>
        private struct ScheduledAction
        {
            public ulong ExpectedResponseID;
            public ActionWrapper Action;

            public ScheduledAction( ulong expectedResponseID, ActionWrapper action )
            {
                ExpectedResponseID = expectedResponseID;
                Action = action;
            }
        }

        /// <summary>
        /// Queue of scheduled actions. Each scheduled action has unique ID, which is incremented on allocation.
        /// </summary>
        private Queue<ScheduledAction> _actions = new Queue<ScheduledAction>();

        /// <summary>
        /// Helper structure to cache interop calls without constructing message yet.
        /// </summary>
        private class ScheduledInteropCall
        {
            public ulong MessageID;
            public string Expression;
        }

        /// <summary>
        /// Cached interop calls, which are pending to be sent.
        /// </summary>
        private BlockingCollection<ScheduledInteropCall> _pendingCalls =
            new BlockingCollection<ScheduledInteropCall>( new ConcurrentQueue<ScheduledInteropCall>() );

        private Task _sendingTask;

        /// <summary>
        /// A way to communicate with engine service.
        /// </summary>
        private IChannel _engineMainChannel;
        private IChannel _editorChannel;
        private IViewUpdateScheduler _viewUpdateScheduler;
        private InteropParser _interopParser;
        private Dictionary<uint, NotifierListeners> _notifierEventHandlers = new Dictionary<uint, NotifierListeners>();

        /// <summary>
        /// Internal delegate for SendAck.
        /// </summary>
        /// <param name="pathHashCode"></param>
        /// <param name="message"></param>
        private delegate void OnInteropResponseEventHandler( int pathHashCode, comm.Message message );

        /// <summary>
        /// Interop debugger
        /// </summary>
        public InteropDebugger InteropDebugger { get; private set; }

        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="channel">A way to communicate with engine service.</param>
        public InteropProtocol( IChannel engineMainChannel, IChannel editorChannel, IViewUpdateScheduler viewUpdateScheduler )
        {
            _engineMainChannel = engineMainChannel;
            _engineMainChannel.RegisterListener( OnMessage );

            _editorChannel = editorChannel;
            _editorChannel.RegisterListener( OnRequestToEditor );

            _viewUpdateScheduler = viewUpdateScheduler;

            _interopParser = new InteropParser();

            InteropDebugger = new InteropDebugger();

            StartSendingThread();
        }

        /// <summary>
        /// Uninitialization of the protocol.
        /// </summary>
        private void Uninitialize()
        {
            _editorChannel.UnregisterListener( OnRequestToEditor );
            _engineMainChannel.UnregisterListener( OnMessage );
        }

        /// <summary>
        /// Register notifier handler.
        /// </summary>
        /// <param name="notifierID"></param>
        /// <param name="handler"></param>
        /// <returns></returns>
        public void RegisterNotifierEventHandler( uint notifierID, OnNotifyEventHandler handler )
        {
            bool contains = _notifierEventHandlers.ContainsKey( notifierID );

            if ( !contains )
            {
                _notifierEventHandlers.Add( notifierID, new NotifierListeners() );
            }

            _notifierEventHandlers[notifierID].OnNotifyEvent += handler;
        }

        /// <summary>
        /// Unregister notifier handler.
        /// </summary>
        /// <param name="notifierID"></param>
        /// <param name="handler"></param>
        public void UnregisterNotifierEventHandler( uint notifierID, OnNotifyEventHandler handler )
        {
            bool success = _notifierEventHandlers.ContainsKey( notifierID );
            if ( success )
                _notifierEventHandlers[notifierID].OnNotifyEvent -= handler;
        }

        /// <summary>
        /// Starts a thread for message sending.
        /// </summary>
        private void StartSendingThread()
        {
            _sendingTask = new Task( () =>
            {
                while (true)
                {
                    ScheduledInteropCall scheduledInteropCall;
                    if ( _pendingCalls.TryTake( out scheduledInteropCall, 10 ) )
                    {
                        comm.Message message;
                        message = _interopParser.ConstructInterop( scheduledInteropCall.MessageID, scheduledInteropCall.Expression );
                        _engineMainChannel.Send( message );
                    }
                }
            }, TaskCreationOptions.LongRunning );

            _sendingTask.Start();
        }

        /// <summary>
        /// Executes general action (it can be set, get (although those two have also SetValue GetValue methods), add, insert, delete)
        /// </summary>
        /// <param name="expression">Action to be performed on interop object.</param>
        /// <returns>Unique message id for response tracking.</returns>
        public ulong InternalExecuteExpression( string expression )
        {
            ulong messageID = MessageIDAllocator.GetNewMessageID();

            ScheduledInteropCall call = new ScheduledInteropCall();
            call.MessageID = messageID;
            call.Expression = expression;

            _pendingCalls.Add( call );

            return messageID;
        }

        /// <summary>
        /// Destructor.
        /// </summary>
        ~InteropProtocol()
        {
            Uninitialize();
        }

        /// <summary>
        /// Disposer.
        /// </summary>
        public void Dispose()
        {
            Uninitialize();
            GC.SuppressFinalize( this );
        }

        /// <summary>
        /// Internal context-less message handler.
        /// </summary>
        /// <param name="message"></param>
        /// <param name="response"></param>
        private void OnMessage( comm.Message message, IResponse response )
        {
            InteropResult result = _interopParser.ParseInteropResultMessage( message );
            ulong ersponseid = _interopParser.ExtractResponseId( message );

            if ( result.InteropType == EInteropType.Exception )
            {
                _viewUpdateScheduler.Schedule( () =>
                {
                    OnInteropException( ersponseid, result );
                } );
            }
            else
            {
                _viewUpdateScheduler.Schedule( () =>
                {
                    OnInteropResponse( ersponseid, result );
                } );
            }
        }

        /// <summary>
        /// Request to editor (like notifier messages etc.), so it's not a response to request sent to engine. It's engine requesting something.
        /// </summary>
        /// <param name="message"></param>
        /// <param name="?"></param>
        private void OnRequestToEditor( comm.Message message, IResponse response )
        {
            if ( message.IsA( comm.EMessageID.InteropNotifierEvent ) )
                OnNotifierEvent( (comm.InteropNotifierEvent)message, response );
        }

        /// <summary>
        /// On notifier event.
        /// </summary>
        /// <param name="message"></param>
        /// <param name="response"></param>
        private void OnNotifierEvent( comm.InteropNotifierEvent message, IResponse response )
        {
            uint notifierID = message.notifierId;
            if ( _notifierEventHandlers.ContainsKey( notifierID ) )
            {
                _viewUpdateScheduler.Schedule( () =>
                {
                    string str = System.Text.Encoding.ASCII.GetString( message.notiferArg.ToArray(), 0, message.notiferArg.Count );

                    _notifierEventHandlers[notifierID].Notify( str );
                } );
            }
            else
            {
                _viewUpdateScheduler.Schedule( () =>
                {
                    InteropDebugger.Log( new InteropWarning( string.Format( "Notifier number {0} there is no receiver on editor side!", notifierID ) ) );
                } );
            }
        }

        /// <summary>
        /// Internal on interop response event handler.
        /// </summary>
        private void OnInteropResponse( ulong responseID, InteropResult result )
        {
            while ( _actions.Count > 0 )
            {
                // Just peek the first action in queue, to check if response is for this or future action.
                ScheduledAction scheduledAction = _actions.Peek();

                // Check if it's for this action or for any future action.
                if ( scheduledAction.ExpectedResponseID > responseID )
                    break;

                // Check if it's for this action
                scheduledAction = _actions.Dequeue();
                InteropDebugger.Stats.ActionsWaitingForResponse = _actions.Count;

                if ( scheduledAction.ExpectedResponseID != responseID )
                {
                    // It's not for this action, but we won't receive response for it, so report exception.
                    SendInteropExceptionResponse( scheduledAction, "Action executed out of order." );
                    InteropDebugger.Stats.ActionsOutOfOrder++;
                    continue;   // And let's find that bastard!
                }
                else
                {
                    // Hurray, it's for me!
                    scheduledAction.Action.OnResponse( result );

                    InteropDebugger.Stats.ActionsHandled++;
                    InteropDebugger.Stats.UpdateTimers( DateTime.Now.TimeOfDay.TotalSeconds - scheduledAction.Action.DebugInfo.CreationTime );
                    if ( scheduledAction.Action.IgnoreDebugger == false )
                    {
                        InteropDebugger.Log( new InteropDebugMessage( string.Format( "Correct response: {0}", result.Data ), scheduledAction.Action.DebugInfo ) );
                    }
                    break;
                }
            }
        }

        /// <summary>
        /// Internal on interop exception event handler.
        /// </summary>
        private void OnInteropException( ulong responseID, InteropResult result )
        {
            while ( _actions.Count > 0 )
            {
                // Just peek the first action in queue, to check if response is for this or future action.
                ScheduledAction scheduledAction = _actions.Peek();

                // Check if it's for this action or for any future action.
                if ( scheduledAction.ExpectedResponseID > responseID )
                    break;

                // Check if it's for this action
                scheduledAction = _actions.Dequeue();
                InteropDebugger.Stats.ActionsWaitingForResponse = _actions.Count;

                if ( scheduledAction.ExpectedResponseID != responseID )
                {
                    // It's not for this action, but we won't receive response for it, so report exception.
                    SendInteropExceptionResponse( scheduledAction, "Action executed out of order." );
                    InteropDebugger.Stats.ActionsOutOfOrder++;
                    continue;   // And let's find that bastard!
                }
                else
                {
                    SendInteropExceptionResponse( scheduledAction, result.Data );   // Hurray, it's for me!
                    InteropDebugger.Stats.RemoteActionExceptions++;
                    InteropDebugger.Stats.UpdateTimers( DateTime.Now.TimeOfDay.TotalSeconds - scheduledAction.Action.DebugInfo.CreationTime );
                    break;
                }
            }
        }

        /// <summary>
        /// Informs listeners about interop protocol exception
        /// </summary>
        /// <param name="scheduledAction">Interop scheduled action</param>
        /// <param name="message">Additional message for this exception</param>
        private void SendInteropExceptionResponse( ScheduledAction scheduledAction, string message )
        {
            ProtocolException newEx = new ProtocolException( message, scheduledAction.Action.DebugInfo );
            scheduledAction.Action.OnException( newEx );
            if ( scheduledAction.Action.IgnoreDebugger == false )
            {
                InteropDebugger.Log( newEx );
            }
        }

        public ulong Execute( ActionWrapper action )
        {
#if DEBUG
            action.DebugInfo = new InteropDebugInfo( action.Expression, new StackTrace( true ).GetFrames() );
#else
            action.DebugInfo = new InteropDebugInfo( action.Expression, null );
#endif

            lock ( _actions )
            {
                ulong expectedResponseID = InternalExecuteExpression( action.Expression );
                _actions.Enqueue( new ScheduledAction( expectedResponseID, action ) );
                InteropDebugger.Stats.ActionsWaitingForResponse = _actions.Count;
                return expectedResponseID;
            }
        }
    }
}
